{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c66f34be",
   "metadata": {},
   "source": [
    "# PDF\n",
    "\n",
    "[Portable Document Format (PDF)](https://en.wikipedia.org/wiki/PDF), ISO 32000으로 표준화된 파일 형식은 Adobe가 1992년에 문서를 제시하기 위해 개발했으며, 이는 응용 소프트웨어, 하드웨어 및 운영 시스템에 독립적인 방식으로 텍스트 서식 및 이미지를 포함합니다.\n",
    "\n",
    "이 가이드는 `PDF` 문서를 LangChain [Document](https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html#langchain_core.documents.base.Document) 형식으로 로드하는 방법을 다룹니다. 이 형식은 다운스트림에서 사용됩니다.\n",
    "\n",
    "LangChain은 다양한 PDF 파서와 통합됩니다. 일부는 간단하고 상대적으로 저수준이며, 다른 일부는 OCR 및 이미지 처리를 지원하거나 고급 문서 레이아웃 분석을 수행합니다. \n",
    "\n",
    "올바른 선택은 사용자의 애플리케이션에 따라 달라집니다.\n",
    "\n",
    "**참고**\n",
    "\n",
    "- [LangChain 도큐먼트](https://python.langchain.com/v0.1/docs/modules/data_connection/document_loaders/pdf/)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3883eda",
   "metadata": {},
   "source": [
    "## AutoRAG 팀에서의 PDF 실험\n",
    "\n",
    "AutoRAG 에서 진행한 실험을 토대로 작성한 순위표\n",
    "\n",
    "아래 표기된 숫자는 등수를 나타냅니다. (The lower, the better)\n",
    "\n",
    "| | PDFMiner | PDFPlumber | PyPDFium2 | PyMuPDF | PyPDF2 |\n",
    "|----------|:---------:|:----------:|:---------:|:-------:|:-----:|\n",
    "| Medical  | 1         | 2          | 3         | 4       | 5     |\n",
    "| Law      | 3         | 1          | 1         | 3       | 5     |\n",
    "| Finance  | 1         | 2          | 2         | 4       | 5     |\n",
    "| Public   | 1         | 1          | 1         | 4       | 5     |\n",
    "| Sum      | 5         | 5          | 7         | 15      | 20    |\n",
    "\n",
    "출처: [AutoRAG Medium 블로그](https://velog.io/@autorag/PDF-%ED%95%9C%EA%B8%80-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%B6%94%EC%B6%9C-%EC%8B%A4%ED%97%98#%EC%B4%9D%ED%8F%89)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1907c14b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f023d22",
   "metadata": {},
   "source": [
    "## 실습에 활용한 문서\n",
    "\n",
    "소프트웨어정책연구소(SPRi) - 2023년 12월호\n",
    "\n",
    "- 저자: 유재흥(AI정책연구실 책임연구원), 이지수(AI정책연구실 위촉연구원)\n",
    "- 링크: https://spri.kr/posts/view/23669\n",
    "- 파일명: `SPRI_AI_Brief_2023년12월호_F.pdf`\n",
    "\n",
    "**참고**: 위의 파일은 `data` 폴더 내에 다운로드 받으세요"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5180451c",
   "metadata": {},
   "outputs": [],
   "source": [
    "FILE_PATH = \"./data/SPRI_AI_Brief_2023년12월호_F.pdf\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1978423",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_metadata(docs):\n",
    "    if docs:\n",
    "        print(\"[metadata]\")\n",
    "        print(list(docs[0].metadata.keys()))\n",
    "        print(\"\\n[examples]\")\n",
    "        max_key_length = max(len(k) for k in docs[0].metadata.keys())\n",
    "        for k, v in docs[0].metadata.items():\n",
    "            print(f\"{k:<{max_key_length}} : {v}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c404e7d",
   "metadata": {},
   "source": [
    "## PyPDF\n",
    "\n",
    "여기에서는 `pypdf`를 사용하여 PDF를 문서 배열로 로드하며, 각 문서는 `page` 번호와 함께 페이지 내용 및 메타데이터를 포함합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9673857",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 설치\n",
    "# !pip install -qU pypdf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aa20caf2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PyPDFLoader\n",
    "\n",
    "# 파일 경로 설정\n",
    "loader = PyPDFLoader(FILE_PATH)\n",
    "\n",
    "# PDF 로더 초기화\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[10].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "453f2103",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 메타데이터 출력\n",
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "96845496",
   "metadata": {},
   "source": [
    "### PyPDF(OCR)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c39e3c86",
   "metadata": {},
   "source": [
    "일부 PDF에는 스캔된 문서나 그림 내에 텍스트 이미지가 포함되어 있습니다. `rapidocr-onnxruntime` 패키지를 사용하여 이미지에서 텍스트를 추출할 수도 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "009b9e3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 설치\n",
    "# !pip install -qU rapidocr-onnxruntime"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b5334000",
   "metadata": {},
   "outputs": [],
   "source": [
    "# PDF 로더 초기화, 이미지 추출 옵션 활성화\n",
    "loader = PyPDFLoader(\"https://arxiv.org/pdf/2103.15348.pdf\", extract_images=True)\n",
    "\n",
    "# PDF 페이지 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 페이지 내용 접근\n",
    "print(docs[4].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0fe6caa9",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a191d5c",
   "metadata": {},
   "source": [
    "## PyMuPDF"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea88bca1",
   "metadata": {},
   "source": [
    "**PyMuPDF** 는 속도 최적화가 되어 있으며, PDF 및 해당 페이지에 대한 자세한 메타데이터를 포함하고 있습니다. 페이지 당 하나의 문서를 반환합니다:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4ac56d57",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 설치\n",
    "# !pip install -qU pymupdf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47e7a947",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PyMuPDFLoader\n",
    "\n",
    "# PyMuPDF 로더 인스턴스 생성\n",
    "loader = PyMuPDFLoader(FILE_PATH)\n",
    "\n",
    "# 문서 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[10].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bbca8760",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c89745c9",
   "metadata": {},
   "source": [
    "## Unstructured"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72199f11",
   "metadata": {},
   "source": [
    "[Unstructured](https://unstructured-io.github.io/unstructured/)는 Markdown이나 PDF와 같은 비구조화된 또는 반구조화된 파일 형식을 다루기 위한 공통 인터페이스를 지원합니다. \n",
    "\n",
    "LangChain의 [UnstructuredPDFLoader](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.pdf.UnstructuredPDFLoader.html)는 Unstructured와 통합되어 PDF 문서를 LangChain [Document](https://api.python.langchain.com/en/latest/documents/langchain_core.documents.base.Document.html) 객체로 파싱합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6cc687b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 설치\n",
    "# !pip install -qU unstructured"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "40cb362e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import UnstructuredPDFLoader\n",
    "\n",
    "# UnstructuredPDFLoader 인스턴스 생성\n",
    "loader = UnstructuredPDFLoader(FILE_PATH)\n",
    "\n",
    "# 데이터 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[0].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "926b929e",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3115d62b",
   "metadata": {},
   "source": [
    "내부적으로 비정형에서는 텍스트 청크마다 서로 다른 \"**요소**\"를 만듭니다. 기본적으로 이들은 결합되어 있지만 `mode=\"elements\"`를 지정하여 쉽게 분리할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6f97007",
   "metadata": {},
   "outputs": [],
   "source": [
    "# UnstructuredPDFLoader 인스턴스 생성(mode=\"elements\")\n",
    "loader = UnstructuredPDFLoader(FILE_PATH, mode=\"elements\")\n",
    "\n",
    "# 데이터 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[0].page_content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19ab703e",
   "metadata": {},
   "source": [
    "이 특정 문서에 대한 전체 요소 유형 집합을 참조하세요"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0fd7976",
   "metadata": {},
   "outputs": [],
   "source": [
    "set(doc.metadata[\"category\"] for doc in docs)  # 데이터 카테고리 추출"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ec0c096",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f5097872",
   "metadata": {},
   "source": [
    "## PyPDFium2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18c84bf5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PyPDFium2Loader\n",
    "\n",
    "# PyPDFium2 로더 인스턴스 생성\n",
    "loader = PyPDFium2Loader(FILE_PATH)\n",
    "\n",
    "# 데이터 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[10].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d4cd8966",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d2b2a6a",
   "metadata": {},
   "source": [
    "## PDFMiner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5feac159",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PDFMinerLoader\n",
    "\n",
    "# PDFMiner 로더 인스턴스 생성\n",
    "loader = PDFMinerLoader(FILE_PATH)\n",
    "\n",
    "# 데이터 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[0].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65a85f23",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d918ef7",
   "metadata": {},
   "source": [
    "**PDFMiner**를 사용하여 HTML 텍스트 생성\n",
    "\n",
    "이 방법은 출력된 HTML 콘텐츠를 `BeautifulSoup`을 통해 파싱함으로써 글꼴 크기, 페이지 번호, PDF 헤더/푸터 등에 대한 보다 구조화되고 풍부한 정보를 얻을 수 있게 하여 텍스트를 의미론적으로 섹션으로 분할하는 데 도움이 될 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d299c2be",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PDFMinerPDFasHTMLLoader\n",
    "\n",
    "# PDFMinerPDFasHTMLLoader 인스턴스 생성\n",
    "loader = PDFMinerPDFasHTMLLoader(FILE_PATH)\n",
    "\n",
    "# 문서 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 내용 출력\n",
    "print(docs[0].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0aacfd09",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "df728c9d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from bs4 import BeautifulSoup\n",
    "\n",
    "soup = BeautifulSoup(docs[0].page_content, \"html.parser\")  # HTML 파서 초기화\n",
    "content = soup.find_all(\"div\")  # 모든 div 태그 검색"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15d75111",
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "\n",
    "cur_fs = None\n",
    "cur_text = \"\"\n",
    "snippets = []  # 동일한 글꼴 크기의 모든 스니펫 수집\n",
    "for c in content:\n",
    "    sp = c.find(\"span\")\n",
    "    if not sp:\n",
    "        continue\n",
    "    st = sp.get(\"style\")\n",
    "    if not st:\n",
    "        continue\n",
    "    fs = re.findall(\"font-size:(\\d+)px\", st)\n",
    "    if not fs:\n",
    "        continue\n",
    "    fs = int(fs[0])\n",
    "    if not cur_fs:\n",
    "        cur_fs = fs\n",
    "    if fs == cur_fs:\n",
    "        cur_text += c.text\n",
    "    else:\n",
    "        snippets.append((cur_text, cur_fs))\n",
    "        cur_fs = fs\n",
    "        cur_text = c.text\n",
    "snippets.append((cur_text, cur_fs))\n",
    "# 중복 스니펫 제거 전략 추가 가능성 (PDF의 헤더/푸터가 여러 페이지에 걸쳐 나타나므로 중복 발견 시 중복 정보로 간주 가능)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8061d2e5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.documents import Document\n",
    "\n",
    "cur_idx = -1\n",
    "semantic_snippets = []\n",
    "# 제목 가정: 높은 글꼴 크기\n",
    "for s in snippets:\n",
    "    # 새 제목 판별: 현재 스니펫 글꼴 > 이전 제목 글꼴\n",
    "    if (\n",
    "        not semantic_snippets\n",
    "        or s[1] > semantic_snippets[cur_idx].metadata[\"heading_font\"]\n",
    "    ):\n",
    "        metadata = {\"heading\": s[0], \"content_font\": 0, \"heading_font\": s[1]}\n",
    "        metadata.update(docs[0].metadata)\n",
    "        semantic_snippets.append(Document(page_content=\"\", metadata=metadata))\n",
    "        cur_idx += 1\n",
    "        continue\n",
    "\n",
    "    # 동일 섹션 내용 판별: 현재 스니펫 글꼴 <= 이전 내용 글꼴\n",
    "    if (\n",
    "        not semantic_snippets[cur_idx].metadata[\"content_font\"]\n",
    "        or s[1] <= semantic_snippets[cur_idx].metadata[\"content_font\"]\n",
    "    ):\n",
    "        semantic_snippets[cur_idx].page_content += s[0]\n",
    "        semantic_snippets[cur_idx].metadata[\"content_font\"] = max(\n",
    "            s[1], semantic_snippets[cur_idx].metadata[\"content_font\"]\n",
    "        )\n",
    "        continue\n",
    "\n",
    "    # 새 섹션 생성 조건: 현재 스니펫 글꼴 > 이전 내용 글꼴, 이전 제목 글꼴 미만\n",
    "    metadata = {\"heading\": s[0], \"content_font\": 0, \"heading_font\": s[1]}\n",
    "    metadata.update(docs[0].metadata)\n",
    "    semantic_snippets.append(Document(page_content=\"\", metadata=metadata))\n",
    "    cur_idx += 1\n",
    "\n",
    "print(semantic_snippets[4])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0740f50",
   "metadata": {},
   "source": [
    "## PyPDF 디렉토리\n",
    "\n",
    "디렉토리에서 PDF를 로드하세요"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "93bbbbad",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PyPDFDirectoryLoader\n",
    "\n",
    "# 디렉토리 경로\n",
    "loader = PyPDFDirectoryLoader(\"data/\")\n",
    "\n",
    "# 문서 로드\n",
    "docs = loader.load()\n",
    "\n",
    "# 문서의 개수 출력\n",
    "print(len(docs))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "231ffd1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 문서의 내용 출력\n",
    "print(docs[50].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "05c68b12",
   "metadata": {},
   "outputs": [],
   "source": [
    "# metadata 출력\n",
    "print(docs[50].metadata)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebb92d77",
   "metadata": {},
   "source": [
    "## PDFPlumber\n",
    "\n",
    "PyMuPDF와 마찬가지로, 출력 문서는 PDF와 그 페이지에 대한 자세한 메타데이터를 포함하며, 페이지 당 하나의 문서를 반환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e97bd7f1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PDFPlumberLoader\n",
    "\n",
    "# PDF 문서 로더 인스턴스 생성\n",
    "loader = PDFPlumberLoader(FILE_PATH)\n",
    "\n",
    "# 문서 로딩\n",
    "docs = loader.load()\n",
    "\n",
    "# 첫 번째 문서 데이터 접근\n",
    "print(docs[10].page_content[:300])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e250ac42",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_metadata(docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0c637a9",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
